package io.kiki.stack.http.feign;

import io.kiki.stack.http.feign.Logger.Level;
import io.kiki.stack.http.feign.Request.Options;
import io.kiki.stack.http.feign.codec.Decoder;
import io.kiki.stack.http.feign.codec.Encoder;
import io.kiki.stack.http.feign.codec.ErrorDecoder;

import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.List;

/**
 * Capabilities expose core feign artifacts to implementations so parts of core can be customized
 * around the time the client being built.
 *
 * <p>For instance, capabilities take the {@link Client}, make changes to it and feed the modified
 * version back to feign.
 *
 * @see Metrics5Capability
 */
public interface Capability {

    static Object enrich(Object componentToEnrich, Class<?> capabilityToEnrich, List<Capability> capabilities) {
        return capabilities.stream()
                // invoke each individual capability and feed the result to the next one.
                // This is equivalent to:
                // Capability cap1 = ...;
                // Capability cap2 = ...;
                // Capability cap2 = ...;
                // Contract contract = ...;
                // Contract contract1 = cap1.enrich(contract);
                // Contract contract2 = cap2.enrich(contract1);
                // Contract contract3 = cap3.enrich(contract2);
                // or in a more compact version
                // Contract enrichedContract = cap3.enrich(cap2.enrich(cap1.enrich(contract)));
                .reduce(componentToEnrich, (target, capability) -> invoke(target, capability, capabilityToEnrich), (component, enrichedComponent) -> enrichedComponent);
    }

    static Object invoke(Object target, Capability capability, Class<?> capabilityToEnrich) {
        return Arrays.stream(capability.getClass().getMethods()).filter(method -> method.getName().equals("enrich")).filter(method -> method.getReturnType().isAssignableFrom(capabilityToEnrich)).findFirst().map(method -> {
            try {
                return method.invoke(capability, target);
            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                throw new RuntimeException("Unable to enrich " + target, e);
            }
        }).orElse(target);
    }

    default Client enrich(Client client) {
        return client;
    }

    default AsyncClient<Object> enrich(AsyncClient<Object> client) {
        return client;
    }

    default Retryer enrich(Retryer retryer) {
        return retryer;
    }

    default RequestInterceptor enrich(RequestInterceptor requestInterceptor) {
        return requestInterceptor;
    }

    default ResponseInterceptor enrich(ResponseInterceptor responseInterceptor) {
        return responseInterceptor;
    }

    default ResponseInterceptorChain enrich(ResponseInterceptorChain responseInterceptorChain) {
        return responseInterceptorChain;
    }

    default Logger enrich(Logger logger) {
        return logger;
    }

    default Level enrich(Level level) {
        return level;
    }

    default Contract enrich(Contract contract) {
        return contract;
    }

    default Options enrich(Options options) {
        return options;
    }

    default Encoder enrich(Encoder encoder) {
        return encoder;
    }

    default Decoder enrich(Decoder decoder) {
        return decoder;
    }

    default ErrorDecoder enrich(ErrorDecoder decoder) {
        return decoder;
    }

    default InvocationHandlerFactory enrich(InvocationHandlerFactory invocationHandlerFactory) {
        return invocationHandlerFactory;
    }

    default QueryMapEncoder enrich(QueryMapEncoder queryMapEncoder) {
        return queryMapEncoder;
    }

    default AsyncResponseHandler enrich(AsyncResponseHandler asyncResponseHandler) {
        return asyncResponseHandler;
    }

    default <C> AsyncContextSupplier<C> enrich(AsyncContextSupplier<C> asyncContextSupplier) {
        return asyncContextSupplier;
    }

    default MethodInfoResolver enrich(MethodInfoResolver methodInfoResolver) {
        return methodInfoResolver;
    }
}
